#include<iostream>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<unistd.h>
#include<string>
#include<sys/types.h>
#include<sys/wait.h>
#include<fcntl.h>
#include<ctype.h>

#define FORMAT "[%s@%s %s]# "
#define COMMAND_SIZE 1024

// 命令行参数表
#define MAXARGC 128
char* g_argv[MAXARGC];
int g_argc = 0;

// 环境变量表
#define MAX_ENVS 100
char* g_env[MAX_ENVS];
int g_envc = 0;

// 临时使用
char cwdt[1024];
char cwdenvt[1024];

// 重定向选项
#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3
int redir = NONE_REDIR;
std::string filename;

const char* GetUserName()
{
    const char* user = getenv("USER");
    return user == nullptr ? " " : user;
}

const char* GetHostName()
{
    const char* host = getenv("HOSTNAME");
    return host == nullptr ? " " : host;
}

#define CWD_MAX 1024
char cwd[CWD_MAX];

const char* GetPwd()
{
    // 更新局部变量表
    const char* pwd = getcwd(cwd,sizeof(cwd));
    if(!pwd) return "";
    // 找到全局环境变量表中的PWD
    for(int i = 0;g_env[i];++i)
    {
        if(strncmp(g_env[i],"PWD=",4) == 0)
        {
            // 重新开空间
            char* new_pwd = (char*)malloc(strlen(cwd) + 5); // 包含\0
            // 写入
            snprintf(new_pwd,strlen(cwd) + 5,"PWD=%s",cwd);
            // 添加到全局表
            g_env[i] = new_pwd;
            break;
        }
    }
    return cwd;
}

//const char* GetPwd()
//{
//    //cons pt char* pwd = getenv("PWD"); 当工作路径更新后，要由shell更新环境变量，但是我们写的shell目前还没有这个功能，因此要换getcwd来获取当前的工作路径
//    const char* pwd = getcwd(cwdt,sizeof(cwdt));
//    if(pwd != NULL)
//    {
//        snprintf(cwdenvt,sizeof(cwdenvt),"PWD=%s",cwdt);
//        putenv(cwdenvt);
//    }
//    return pwd == nullptr ? " " : pwd;
//}

const char* GetHome()
{
    const char* home = getenv("HOME");
    return home == nullptr ? "" : home;
}

std::string DirName(const char* pwd)
{
#define SLASH "/" // 分隔符
    std::string dir = pwd;
    if(dir == SLASH) return SLASH; // 在家目录情况
    auto pos = dir.rfind(SLASH);
    if(pos == std::string::npos) return "BUG?";
    return dir.substr(pos+1);
}

void MakeCommandLine(char*cmd_prompt,int size)
{
    // snprintf(cmd_prompt,size,FORMAT,GetUserName(),GetHostName(),GetPwd());// 这样的工作路径是从家目录开始的
    snprintf(cmd_prompt,size,FORMAT,GetUserName(),GetHostName(),DirName(GetPwd()).c_str());// 这样的工作路径是从家目录开始的
}

void PrintCommandPrompt()
{
    // printf("[%s@%s %s]#",GetUserName(),GetHostName(),GetPwd());// [Yuey@hcss-ecs-be40 MyShell]$ 用户名@主机名 工作目录，这些都可以从环境变量中获取
    //printf(FORMAT,GetPwdtUserName(),GetHostName(),GetPwd());// [Yuey@hcss-ecs-be40 MyShell]$ 用户名@主机名 工作目录，这些都可以从环境变量中获取
    char prompt[COMMAND_SIZE];
    MakeCommandLine(prompt,sizeof(prompt));
    printf("%s",prompt);
    fflush(stdout);
}

bool GetCommandLine(char* out,int size)
{
    // ls -a -l,读取这样的字符串
    char* c = fgets(out,size,stdin);
    if(c == nullptr) return false;
    out[strlen(out) - 1] = '\0';// 实际输入是ls -al\n,将最后的\n去除
    if(strlen(out) == 0) return false;// 处理只有回车的情况
    return true;
}

bool CommandParse(char* commadline)
{
    // ls -al 分割为"ls" "-al"
#define SEP " "
    g_argc = 0;
    g_argv[g_argc++] = strtok(commadline,SEP);
    while((bool)(g_argv[g_argc++] = strtok(nullptr,SEP)));
    g_argc--;
    return g_argc > 0 ? true : false;
}

// 退出码
int lastcode = 0;

void Execute()
{
    pid_t id = fork();
    if(id == 0)
    {
        if(redir == NONE_REDIR)
        {

        }
        else if(redir == INPUT_REDIR)
        {
            // 输入重定向
            int fd = open(filename.c_str(),O_RDONLY);
            if(fd < 0) exit(1);
            dup2(fd,0);
        }
        else if(redir == OUTPUT_REDIR)
        {
            int fd = open(filename.c_str(),O_CREAT | O_WRONLY | O_TRUNC,0666);
            if(fd < 0) exit(1);
            dup2(fd,1);
        }
        else if(redir == APPEND_REDIR)
        {
            int fd = open(filename.c_str(),O_CREAT | O_WRONLY | O_APPEND,0666);
            if(fd < 0) exit(1);
            dup2(fd,1);
        }
        // child
        execvp(g_argv[0],g_argv);
    }
    int status = 0;
    pid_t rid = waitpid(id,&status,0);
    if(rid > 0)
    {
        lastcode = WEXITSTATUS(status);// 最近进程的退出码
    }
}

bool Cd()
{
    if(g_argc == 1)// cd命令
    {
        // 返回家目录,不能用进程程序替换，如果用了后面的进程就无法执行了
        std::string home = GetHome();
        if(home.empty()) return true;
        chdir(home.c_str());// 这样更改结束后，命令行提示符的工作路径是没有修改的
    }
    else 
    {
        // cd dest 
        std::string dest = g_argv[1];
        if(dest == "~")
        {
            std::string home = GetHome();
            chdir(home.c_str());
        }
        else  
        {
            chdir(dest.c_str());
        }
    }
    return true;
}

void Echo()
{
    // echo $PATH
    // echo $?
    // echo "Hello world"
    std::string opt = g_argv[1];
    if(g_argc == 2)
    {
        if(opt == "$?")
        {
            std::cout << lastcode << std::endl;
            lastcode = 0;
        }
        else if(opt[0] == '$')
        {
            std::string env_name = opt.substr(1);
            const char* env_value = getenv(env_name.c_str());
            if(env_value)
                std::cout << env_value << std::endl;
        }
        else  
        {
            std::cout<< opt <<std::endl;
        }
    }
    else 
    {
        for(int i = 1; g_argv[i];i++){
            if(i == 1) 
            {
                std::string begin = g_argv[1];
                std::cout << begin.substr(1) << " ";
            }
            else if(i == g_argc - 1)
            {
                std::string end = g_argv[g_argc - 1];
                std::cout << end.substr(0,end.size() - 1) << " ";
            }
            else 
                std::cout << g_argv[i] << " ";
        }
        std::cout << std::endl;
    }
}

bool CheckAndExecBuiltin()
{
    std::string cmd = g_argv[0];
    if(cmd == "cd")
    {
        Cd();
        return true;
    }
    else if(cmd == "echo")
    {
        Echo();
        return true;
    }
    return false;
}

void InitEnv()
{
    extern char** environ;// 1. 通过全局指针初始化环境变量表
    memset(g_env,0,sizeof(g_env));// 防御
    for(int i= 0;environ[i];++i)
    {
        g_env[i] = (char*)malloc(strlen(environ[i]) + 1);// 先开空间
        strcpy(g_env[i],environ[i]);// 再拷贝内容
        g_envc++;
    }
    //g_env[g_envc++] = (char*)"MYENV=******************************"; test 
    g_env[g_envc] = NULL;// 表示结束

    // 2. 导成环境变量
    for(int i = 0;g_env[i];++i)
        putenv(g_env[i]);
    environ = g_env;
}

void clean()
{
    for(int i = 0;g_env[i];++i)
    {
        free(g_env[i]);
        g_env[i] = NULL;
    }
    g_envc = 0;
}

void TrimSpace(char* cmd,int& end)
{
    while(isspace(cmd[end])) 
        end++;
}

void RedirCheck(char* cmd)
{
    redir = NONE_REDIR;
    filename.clear();
    int begin = 0;
    int end = strlen(cmd) - 1;

    // 找重定向符
    while(end > begin)
    {
        char tmp = cmd[end];
        if(tmp == '<')
        {
            // 输入重定向
            cmd[end++] = 0;
            TrimSpace(cmd,end);
            filename = cmd + end;
            redir = INPUT_REDIR;
            break;
        }
        else if(tmp == '>')
        {
            if(cmd[end - 1] == '>')
            {
                // 追加重定向
                cmd[end - 1] = 0;
                redir = APPEND_REDIR;
            }
            else 
            {
                // 输出重定向
                redir = OUTPUT_REDIR;
            }
            cmd[end++] = 0;
            TrimSpace(cmd,end);
            filename = cmd + end;
            break;
        }
        else 
            end--;
    }
}
int main()
{
    // 导入环境变量
    InitEnv();

    // 因为shell跑起来只有在关掉进程后才会结束，所以要写成死循环
    while(1)
    {
        // 1. 输出命令行提示符
        PrintCommandPrompt();
        
        // 2. 获取用户输入的命令,去除\n,并判断是否有命令，将命令转为字符串
        char commandline[COMMAND_SIZE];
        if(!GetCommandLine(commandline,sizeof(commandline)))
            continue;

        // 3. 重定向分析 "ls - a -l > filename" 
        RedirCheck(commandline);
        
        // 4. 解析命令行，制作命令行参数表
        if(!CommandParse(commandline))
            continue;
        
        // 5. 检验是否是内建命令，内建命令不需要创建子进程执行
        if(CheckAndExecBuiltin())
            continue;

        // 6. 执行命令
        Execute();
    }

    clean();

    return 0;
}
